#region Copyright
/// <copyright>
/// Copyright (c) 2007 Ilia Shramko.
/// Released under the terms of the GNU General Public License.
/// </copyright>
#endregion

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Text;

using Osh.Media;

namespace Osh.ZoomPlayer
{
	public class ZoomPlayerApplication : MediaPlayerBase, IZoomPlayer
	{
		private const int MaxNumberOfTracks = 100;
		private const int PlayerMonitoringIntervalMs = 1000; // ms
		private static TraceSource _traceSource = new TraceSource("Osh.ZoomPlayer");
		private TcpClient _playerConnection;
		private Timeline _timeline;
		private ZoomPlayerVideoFile _newMedia;
		private Process _playerProcess = null;

		public ZoomPlayerApplication(string name) : base(name)
		{
			Log.Info(TraceSource, "Created ZoomPlayerApplication object.");

			_playerConnection = new TcpClient();

			_timeline = new Timeline(this);

			base.PlayerMonitoringInterval = PlayerMonitoringIntervalMs;
		}

		#region MediaPlayerBase Overrides
		public override void PlayMedia(IMedia media)
		{
			if(media == null)
				throw new ArgumentNullException("media");

			string fileDirectory = media.Attributes[MediaAttributeNames.FileDirectory] as string;
			string fileName = media.Attributes[MediaAttributeNames.FileName] as string;
			if(!string.IsNullOrEmpty(fileDirectory) && !string.IsNullOrEmpty(fileName))
			{
				string filePath = Path.Combine(fileDirectory, fileName);
				SendCommand(CommandCode.PlayFile, filePath);
			}
		}

		public override void Play()
		{
			SendCommand(CommandCode.CallZPFunction, "fnPlay");
		}

		public override void Pause()
		{
			SendCommand(CommandCode.CallZPFunction, "fnPause");
		}

		public override void Stop()
		{
			SendCommand(CommandCode.CallZPFunction, "fnStop");
		}

		protected override PlayerState GetPlayerState()
		{
			if(this.State == PlayerState.Closed || _playerProcess == null)
			{
				Process[] processes = System.Diagnostics.Process.GetProcessesByName("zplayer");
				if(processes.Length == 0)
					return PlayerState.Closed;

				if(!EnsureConnected())
					return PlayerState.Closed;

				_playerProcess = processes[0];
				return PlayerState.Ready;
			}

			if(!IsProcessAlive(_playerProcess))
			{
				_playerProcess = null;
				return PlayerState.Closed;
			}
			return this.State;
		}

		protected override void OnStart()
		{
			// Delay reading ZoomPlayer message events until we completely started.
			StartReading(_playerConnection.GetStream());

			base.OnStart();
		}

		protected override void OnExit()
		{
			Disconnect();

			base.OnExit();
		}
		#endregion MediaPlayerBase Overrides

		public void SendCommand(CommandCode code, string command)
		{
			if(!EnsureConnected())
				return;

			command = ((int)code).ToString().PadLeft(4, '0') + " " + command + "\r\n";
			Log.Write(TraceSource, "Sending {0} command...", command);

			byte[] bytes = Encoding.UTF8.GetBytes(command);
			int send = _playerConnection.Client.Send(bytes);
			if(bytes.Length != send)
				Log.Error(TraceSource, "Send {0} out of {1} bytes of the command.", send, bytes.Length);
		}

		private void OnMessage(int code, string message)
		{
			ParseMessage((MessageCode)code, message);
			if(this.Message != null)
				this.Message(this, new MessageArgs((MessageCode)code, message));
		}

		private void ParseMessage(MessageCode code, string message)
		{
			if(code == MessageCode.StateChange)
			{
				int stateInt;
				if(Int32.TryParse(message, out stateInt))
					ChangeState((ZoomPlayerState)stateInt);
			}
			else if(code == MessageCode.VideoResolution)
			{
				this.NewMedia = null;
				this.NewMedia.Attributes[MediaAttributeNames.MediaType] = MediaType.Video.ToString().ToLower();
				this.NewMedia.Attributes[MediaAttributeNames.VideoWidth] = Utils.SubstringBefore(message, 'x');
				this.NewMedia.Attributes[MediaAttributeNames.VideoHeight] = Utils.SubstringAfter(message, 'x');
			}
			else if(code == MessageCode.VideoFrameRate)
			{
				this.NewMedia.Attributes[MediaAttributeNames.VideoFrameRate] =
					Math.Round(Double.Parse(message, CultureInfo.CurrentCulture), 3);
			}
			else if(code == MessageCode.FileName)
			{
				Uri uri;
				if(Uri.TryCreate(message, UriKind.Absolute, out uri))
					this.NewMedia.SetUri(uri);

				this.NewMedia.Attributes[MediaAttributeNames.FileName] = Path.GetFileName(message);
				this.NewMedia.Attributes[MediaAttributeNames.FileNameWithoutExtension] =
					Path.GetFileNameWithoutExtension(message);
				this.NewMedia.Attributes[MediaAttributeNames.FileType] = Utils.SubstringAfter(Path.GetExtension(message), '.');
				this.NewMedia.Attributes[MediaAttributeNames.FileDirectory] = Path.GetDirectoryName(message);
			}
			else if(code == MessageCode.PlaylistPosition)
			{
				this.NewMedia.Attributes[MediaAttributeNames.PlaylistIndex] = Int32.Parse(Utils.SubstringBefore(message, '/'));

				// PlaylistPosition position is the last message in the sequence
				// VideoResolution, VideoFrameRate, FileName, PlaylistPosition, StateChange (Playing)
				// which precedes a new file playback.
				IMedia newMedia = this.NewMedia;
				this.NewMedia = null;
				base.CurrentMedia = newMedia; // Set and fire MediaChange event
			}
			else if(code == MessageCode.DvdUniqueString)
			{
				// Ok, it's gonna be a DVD
				// The message sequence is going to be as the following
				// DvdUniqueString, [2700], AudioVolume, DvdAudioCount, DvdAudioChange, DvdAudioName*,
				// DvdAngleCount, DvdAngleChange, DvdTitleCount, DvdTitleChange, DvdARModeChange,
				// DvdSubtitleCount, DvdSubtitleChange, DvdSubtitleDisabled,
				// DvdChapterCount, DvdChapterChange, DvdMenuMode...
				base.CurrentMedia = null;
				this.NewMedia = null;
				this.NewMedia.Attributes[MediaAttributeNames.MediaType] = MediaType.Dvd.ToString().ToLower();
				this.NewMedia.Attributes[MediaAttributeNames.DVDID] = Utils.SubstringAfter(message, '.');
			}
			else if(code == MessageCode.DvdAudioCount)
			{
				int numberOfTracks = Math.Min(Int32.Parse(message), MaxNumberOfTracks);
				this.NewMedia.Attributes[MediaAttributeNames.AudioCount] = numberOfTracks;
				this.NewMedia.Attributes[MediaAttributeNames.AudioNames] = new string[numberOfTracks];
			}
			else if(code == MessageCode.DvdAudioName)
			{
				int trackNumber;
				if(!Int32.TryParse(Utils.SubstringBefore(message, ' '), out trackNumber))
					return;

				string trackName = Utils.SubstringAfter(message, ' ');

				string[] trackNames = (string[])this.NewMedia.Attributes[MediaAttributeNames.AudioNames];
				if(trackNames == null || trackNumber >= trackNames.Length)
					return;

				(this.NewMedia.Attributes[MediaAttributeNames.AudioNames] as string[])[trackNumber] = trackName;
			}
			else if(code == MessageCode.DvdAudioChange)
			{
				int trackNumber;
				if(!Int32.TryParse(message, out trackNumber))
					return;

				string[] trackNames = (string[])this.NewMedia.Attributes[MediaAttributeNames.AudioNames];
				if(trackNames == null || trackNumber >= trackNames.Length)
					return;

				this.NewMedia.Attributes[MediaAttributeNames.AudioName] = trackNames[trackNumber];
			}
			else if(code == MessageCode.DvdAudioChange)
			{
				if(this.CurrentMedia == null)
				{
					IMedia newMedia = this.NewMedia;
					this.NewMedia = null;
					base.CurrentMedia = newMedia; // Set and fire MediaChange event
				}
			}
		}

		private ZoomPlayerVideoFile NewMedia
		{
			get
			{
				if(_newMedia == null)
					_newMedia = new ZoomPlayerVideoFile(this);
				return _newMedia;
			}
			set { _newMedia = value; }
		}

		private void ChangeState(ZoomPlayerState zpState)
		{
			PlayerState state;
			switch(zpState)
			{
				case ZoomPlayerState.Closed:
					// Ignore Close state because of often false reports.
					// Player will be put in Closed state on process exit.
					state = PlayerState.Ready; break;
				case ZoomPlayerState.Stoped: state = PlayerState.Ready; break;
				case ZoomPlayerState.Paused: state = PlayerState.Paused; break;
				case ZoomPlayerState.Playing: state = PlayerState.Playing; break;
				default: return;
			}
			this.State = state;
		}

		private bool EnsureConnected()
		{
			if(_playerConnection.Connected)
				return true;
			try
			{
				Log.Info(TraceSource, "Connecting to ZoomPlayer ({0}:{1})...",
					ZoomPlayerSection.Current.Host, ZoomPlayerSection.Current.Port);

				//_playerConnection.Connect(ZoomPlayerSection.Current.Host, ZoomPlayerSection.Current.Port);
				IAsyncResult result = _playerConnection.BeginConnect(ZoomPlayerSection.Current.Host, ZoomPlayerSection.Current.Port, null, null);
				if(!result.AsyncWaitHandle.WaitOne(1000))
					_playerConnection.Close();

				return _playerConnection.Connected;
			}
			catch(Exception e)
			{
				Log.Exception(TraceSource, e);
				Disconnect();
			}
			return false;
		}

		private void Disconnect()
		{
			Log.Info(TraceSource, "Disconnecting from ZoomPlayer...");
			try
			{
				TcpClient client = _playerConnection;
				_playerConnection = new TcpClient();
				if(client != null)
					client.Close();
			}
			catch(Exception e)
			{
				Log.Exception(TraceSource, e);
			}
		}

		private void StartReading(NetworkStream stream)
		{
			Log.Info(TraceSource, "Starting ZoomPlayer events reader...");

			AsyncLineReader reader = new AsyncLineReader(stream);
			reader.LineRead += new ReadLineEventHandler(OnNewLineRead);
			reader.StartReading();
		}

		private void OnNewLineRead(object sender, string line)
		{
			if(line.Length < 6)
			{
				Log.Error(TraceSource, "ZoomPlayer message: " + line);
				return;
			}

			int code;
			if(!Int32.TryParse(line.Substring(0, 4), out code))
			{
				Log.Error(TraceSource, "ZoomPlayer message: " + line);
				return;
			}

			if(!this.IsRunning)
			{
				Log.Warning(TraceSource, "ZoomPlayer message arrived before the application completely started.");
				return;
			}

			string message = line.Substring(5);
			string codeName = "?";
			if(Enum.IsDefined(typeof(MessageCode), code))
				codeName = Enum.GetName(typeof(MessageCode), code);

			Log.Write(TraceSource, "ZoomPlayer message: {0} [{1}] {2}", code, codeName, message);

			OnMessage(code, message);
		}

		#region Osh.Media.IMediaPlayer
		public override ITimeline Timeline
		{
			get { return _timeline; }
		}
		#endregion Osh.Media.IMediaPlayer

		#region IZoomPlayer
		public event EventHandler<MessageArgs> Message;
		#endregion IZoomPlayer

		protected override TraceSource TraceSourceInternal
		{
			get { return ZoomPlayerApplication.TraceSource; }
		}

		public static TraceSource TraceSource
		{
		    get { return _traceSource; }
		}
	}
}
